package com.dj.ms.aop;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dj.common.constants.ResponseStatus;
import com.dj.common.exception.CentralException;
import com.dj.common.exception.CentralRunException;
import com.dj.common.request.CentralRequest;
import com.dj.common.support.ExceptionHandler;
import com.dj.common.support.HttpRequestWrapper;
import com.dj.ms.annotation.BuildOperateLog;
import com.dj.ms.annotation.OperateKey;
import com.dj.ms.event.EventNoTaskBusHandler;
import com.dj.ms.session.Principal;
import com.dj.ms.session.PrincipalHandler;
import com.dj.ms.support.log.OperateLogMetadata;
import com.dj.ms.support.log.OperateLogPutEvent;
import com.netflix.client.ClientException;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * <br>
 * <p>
 *     德基微服务AOP
 *     定制是否开启
 *          @EnableDeJiWebAop（APP层使用）
 *          @EnableDeJiMsAop（微服务base api调用）
 * </p>
 *
 * <br>
 * @author ZJG
 * @version 1.0
 * @date 2021/7/9 上午11:30
 */
@Slf4j
@Aspect
@Component
public class DeJiMicroServiceAspect {

    @Resource
    private MessageSource messageSource;

    @PostConstruct
    public void init() {
        log.info("DeJiMicroServiceAspect startup was successful.");
    }

    @Pointcut("execution(public * com.dj..endpoint.*FeignServiceImpl.*(..))")
    public void cutFeignApi() {}

    @Pointcut("execution(public * com.dj..controller.*Controller.*(..))")
    public void cutController(){}

    @Pointcut("execution(public * com.dj..controller..*Controller.*(..))")
    public void cutAPPController(){}

    @Pointcut("cutFeignApi() || cutController() || cutAPPController()")
    public void cutAll(){}

    @Order(0)
    @Before("cutAll()")
    public void checkArgs(JoinPoint joinPoint) throws CentralRunException {
        try {
            HttpServletRequest servletRequest = ((ServletRequestAttributes) Objects
                    .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

            BuildOperateLog optAnno = joinPoint.getTarget().getClass().getAnnotation(BuildOperateLog.class);
            if (Objects.nonNull(optAnno)) {
                postLog(null, servletRequest, optAnno, false);
            }

            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            optAnno = method.getAnnotation(BuildOperateLog.class);
            if (Objects.nonNull(optAnno)) {
                postLog(null, servletRequest, optAnno, true);
            }

            Object[] args = joinPoint.getArgs();
            if (ObjectUtils.isNotEmpty(args)) {
                for (Object arg: args) {
                    if (ObjectUtil.isNull(arg)) {
                        continue;
                    }
                    if (arg instanceof HttpServletRequest
                            || arg instanceof HttpServletResponse
                            || arg instanceof MultipartFile) {
                        continue;
                    }
                    if (arg instanceof CentralRequest) {
                        CentralRequest request = (CentralRequest)arg;
                        log.info("aspect pointcut check. request: {}", request);
                        if (Objects.nonNull(request)) {
                            request.verifyParam();
                        }
                    }
                    Class<?> paramObjClass = arg.getClass();
                    BuildOperateLog operateLog = paramObjClass.getAnnotation(BuildOperateLog.class);
                    if (Objects.nonNull(operateLog)) {
                        postLog(paramObjClass, servletRequest, operateLog, false);
                    }
                }
            }
        } catch (Exception e) {
            String msg = String.format("Request parameter validation error. msg:: %s.", e.getMessage());
            log.error("aspect:" + msg);
            throw new CentralRunException(ResponseStatus.HTTP_BAD_REQUEST, msg);
        }
    }

    @Around("cutAll()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodShortNm = "";
        String params = "";
        String result = "";
        try {

            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            methodShortNm = signature.toShortString();
            Method method = signature.getMethod();

            Object[] args = joinPoint.getArgs();
            List<Object> collect = Arrays.stream(args).filter(arg -> !(arg instanceof HttpServletRequest)
                    && !(arg instanceof HttpServletResponse)).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(collect)) {
                params = JSON.toJSONString(collect);
            }

            Object proceed = joinPoint.proceed();
            result = JSON.toJSONString(proceed);

            return proceed;
        } catch (Exception e) {
            result = e.getMessage();
            log.error("API.endpoint.::请求日志异常, 异常原因：[{} ==> {}], msg::{}, e::{}", methodShortNm, params, result, e);
            throw process(e, joinPoint);
        } finally {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("API.endpoint.::接口请求日志：【{}】--【{}】--【{}】", methodShortNm, params, result);
                }
            } catch (Exception e) {
                log.error("API接口返回异常[02]，异常原因：{}", e);
            }
        }
    }

    private CentralRunException process(Exception exception, ProceedingJoinPoint joinPoint) {
        CentralRunException pe;
        try {
            if (exception instanceof ClientException) {
                ClientException clientException = (ClientException) exception;
                String s = "未找到服务";
                if (clientException.getErrorType() == ClientException.ErrorType.GENERAL) {
                    String[] split = clientException.getErrorMessage().split(":");
                    s = split.length > 1? String.format("%s" + s, split[1]): s;
                }
                pe = ExceptionHandler.buildRunEx(ResponseStatus.HTTP_UNAVAILABLE, s);
            } else if (exception instanceof IllegalArgumentException) {
                pe = ExceptionHandler.buildRunEx("请求参数异常");
            } else if (exception instanceof CentralException) {
                CentralException re = (CentralException) exception;
                pe = ExceptionHandler.buildRunEx(re.getCode(), re.getMessage());
            } else if (exception instanceof CentralRunException) {
                CentralRunException re = (CentralRunException) exception;
                pe = ExceptionHandler.buildRunEx(re.getCode(), getMessage(re.getCode(), re.getMessage(), joinPoint.getArgs()));
            } else if (exception instanceof UndeclaredThrowableException) {
                UndeclaredThrowableException re = (UndeclaredThrowableException) exception;
                Throwable undeclaredThrowable = re.getUndeclaredThrowable();
                if (undeclaredThrowable instanceof CentralException) {
                    pe = ExceptionHandler.buildRunEx(undeclaredThrowable);
                } else if (exception instanceof CentralRunException) {
                    CentralRunException ree = (CentralRunException) exception;
                    pe = ExceptionHandler.buildRunEx(ree.getCode(), ree.getMessage());
                } else if (undeclaredThrowable instanceof feign.FeignException.InternalServerError) {
                    feign.FeignException.InternalServerError error = (feign.FeignException.InternalServerError) undeclaredThrowable;
                    pe = reFeignEx(error);
                } else if (undeclaredThrowable instanceof FeignException.InternalServerError) {
                    FeignException.InternalServerError error = (FeignException.InternalServerError) undeclaredThrowable;
                    pe = reFeignEx(error);
                } else if (undeclaredThrowable instanceof FeignException) {
                    FeignException error = (FeignException) undeclaredThrowable;
                    pe = reFeignEx(error);
                } else {
                    pe = ExceptionHandler.buildRunEx("系统异常");
                }
            } else if (exception instanceof feign.FeignException.InternalServerError) {
                feign.FeignException.InternalServerError error = (feign.FeignException.InternalServerError) exception;
                pe = reFeignEx(error);
            } else if (exception instanceof FeignException.InternalServerError) {
                FeignException.InternalServerError error = (FeignException.InternalServerError) exception;
                pe = reFeignEx(error);
            } else if (exception instanceof FeignException) {
                FeignException error = (FeignException) exception;
                pe = reFeignEx(error);
            } else {
                pe = ExceptionHandler.buildRunEx(com.dj.ms.utils.ObjectUtil.reMsg(ExceptionUtils.getMessage(exception)));
                logOtherException(exception, joinPoint);
            }
        } catch (Exception e) {
            pe = ExceptionHandler.buildRunEx(com.dj.ms.utils.ObjectUtil.reMsg(ExceptionUtils.getMessage(e)));
            logOtherException(e, joinPoint);
        }
        return pe;
    }

    private void logOtherException(Exception exception, ProceedingJoinPoint joinPoint) {
        CentralRequest request = null;
        Object[] getArgs = joinPoint.getArgs();
        if (getArgs != null && getArgs.length > 0 && getArgs[0] instanceof CentralRequest) {
            request = (CentralRequest) getArgs[0];
        }
        log.error(joinPoint.toLongString() + "失败, 入参::{}, err::{}", request, exception);
    }

    private String getMessage(int errorId, String errorMsg, Object[] params) {
        try {
            if (StringUtils.isBlank(errorMsg)) {
                String message = messageSource.getMessage(String.valueOf(errorId), params, LocaleContextHolder.getLocale());
                return StrUtil.containsAny(message, "FeignException", "InternalServerError")? "API接口异常": message;
            } else {
                String message = MessageFormat.format(errorMsg, params);
                return StrUtil.containsAny(message, "FeignException", "InternalServerError")? "API接口异常": message;
            }
        } catch (Exception e) {
            log.error("获取异常信息异常", e);
            return StringUtils.defaultIfBlank(errorMsg, "未获取到异常提示信息");
        }
    }

    private CentralRunException reFeignEx(Exception ex) {
        FeignException error = (FeignException) ex;
        Optional<ByteBuffer> errByteBuffer = error.responseBody();
        if (ObjectUtil.isNotNull(errByteBuffer) && errByteBuffer.isPresent()) {
            String respStr = new String(errByteBuffer.get().array(), Charset.forName("UTF-8"));
            respStr.replaceAll("\\[", "").replaceAll("]", "");
            if (JSONUtil.isJson(respStr)) {
                JSONObject obj = JSONObject.parseObject(respStr);
                int code = 500;
                if (ObjectUtil.isNotNull(obj) && obj.containsKey("msg")) {
                    String msg = obj.getString("msg");
                    code = obj.getInteger("code");
                    return ExceptionHandler.buildRunEx(code, StrUtil.containsAny(msg, "FeignException", "InternalServerError")? "API接口异常": msg);
                }
            }
        }
        return ExceptionHandler.buildRunEx("API接口异常");
    }

    public void postLog(Class paramObjClass, HttpServletRequest request, BuildOperateLog operateLogAnno, boolean isMethod) throws BeansException {
        try {
            if (!operateLogAnno.enabled()) {
                return;
            }
            HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
            String objStr = "";
            JSONObject valueObj = null;
            if (Objects.nonNull(paramObjClass)) {
                Object paramObj = paramObjClass.newInstance();
                objStr = JSONObject.toJSONString(paramObj);
            }
            if (StringUtils.isBlank(objStr)) {
                objStr = replaceAllBlank(requestWrapper.getBody());
            }

            valueObj = JSONObject.parseObject(objStr);

            Principal principal = PrincipalHandler.buildSessionSerializableNoEx(request);
            String userId = Objects.nonNull(valueObj)
                    && valueObj.containsKey("userId")
                    && StringUtils.isNotBlank(valueObj.getString("userId")) ? valueObj.getString("userId"): principal.getUserId();
            String userName = Objects.nonNull(valueObj)
                    && valueObj.containsKey("userName")
                    && StringUtils.isNotBlank(valueObj.getString("userName")) ? valueObj.getString("userName"): principal.getUserName();
            String merchantId = Objects.nonNull(valueObj)
                    && valueObj.containsKey("merchantId")
                    && StringUtils.isNotBlank(valueObj.getString("merchantId")) ? valueObj.getString("merchantId"): principal.getMerchantId();
            String storeId = Objects.nonNull(valueObj)
                    && valueObj.containsKey("storeId")
                    && StringUtils.isNotBlank(valueObj.getString("storeId")) ? valueObj.getString("storeId"): principal.getStoreId();

            OperateLogMetadata metadata = OperateLogMetadata
                    .builder()
                    .operationParam(objStr)
                    .operationDesc(operateLogAnno.optDescr())
                    .system(operateLogAnno.bizModule())
                    .operationIp(ServletUtil.getClientIP(request))
                    .userId(userId)
                    .userName(userName)
                    .merchantId(merchantId)
                    .storeId(storeId)
                    .createTime(Long.toString(LocalDateTimeUtil.toEpochMilli(LocalDateTime.now())))
                    .createTimeStr(DateUtil.format(LocalDateTime.now(), DatePattern.UTC_SIMPLE_MS_PATTERN))
                    .build();

            metadata.setOperationKey(operateLogAnno.optKey());
            if (Objects.nonNull(valueObj)
                    && valueObj.containsKey(operateLogAnno.optKey())
                    && Objects.nonNull(valueObj.get(operateLogAnno.optKey()))) {
                metadata.setOperationValue(valueObj.get(operateLogAnno.optKey()));
            }

            if (!isMethod && Objects.nonNull(paramObjClass)) {
                Field[] declaredFields = paramObjClass.getDeclaredFields();
                for (Field declaredField : declaredFields) {
                    OperateKey operateAnno = declaredField.getAnnotation(OperateKey.class);
                    if (Objects.isNull(operateAnno)) {
                        continue;
                    }
                    metadata.setOperationKey(declaredField.getName());
                    if (Objects.nonNull(valueObj)
                            && valueObj.containsKey(metadata.getOperationKey())
                            && Objects.nonNull(valueObj.get(metadata.getOperationKey()))) {
                        metadata.setOperationValue(valueObj.get(metadata.getOperationKey()));
                    }
                    //operateKey 只记录一个即可
                    break;
                }
            }

            // 提交日志记录事件
            OperateLogPutEvent event = OperateLogPutEvent.builder().logMetadata(metadata).build();
            EventNoTaskBusHandler.post(event);
        } catch (Exception e) {
            log.error("log push submit error, ex::{}", e);
        }
    }


    /**
     * 去除特殊符号
     */
    private static Pattern NUMBER_PATTERN = Pattern.compile("\\s*|\t|\r|\n");
    public static String replaceAllBlank(String str) {
        String s = "";
        if (str != null) {
            /*\n 回车(\u000a)
            \t 水平制表符(\u0009)
            \s 空格(\u0008)
            \r 换行(\u000d)*/
            Matcher m = NUMBER_PATTERN.matcher(str);
            s = m.replaceAll("");
        }
        return s;
    }
}
